今日目標,輪流出牌。
打出去之前要先選擇~~
game.js
,我們要先取得使用者點擊的牌,並且在第二次點擊時取消選擇,這邊用 Set 來決定目前選擇的牌是哪些,加入這行程式碼:
var selectedCards = new Set();
selectCard()
、unselectCard()
),加入片段程式碼:
function selectCard(id) {
// 加入剛才建立的 set
selectedCards.add(id);
// 新增被選擇時的特效
$(`#${id}`).addClass("card-selected rounded");
}
function unselectCard(id) {
// 從 set 移除
selectedCards.delete(id);
// 移除特效
$(`#${id}`).removeClass("card-selected rounded");
}
game.css
要新增這段:
.card-selected {
box-shadow: 0 0 .3rem rgb(0, 204, 255)!important;
}
.m-card:focus {
outline: none;
}
generateMyHands()
函數,該函數完整內容為:
function generateMyHands(myHands) {
$(".my-hands").empty();
let index = 0;
for (let cards of myHands) {
let tmp = cards.split("-");
let color = (tmp[0] == SUITS.SPADE || tmp[0] == SUITS.CLUB) ? "#000000" : "#FF0000";
$(".my-hands").append(`
<div class="m-card text-center" style="color: ${color}" id="card-${tmp[0]}-${tmp[1]}" tabindex="${index++}">
<div>
${tmp[0]}
<br>
${tmp[1]}
</div>
</div>
`);
}
relocateMyHands();
// 綁定 click 事件
$(".my-hands .m-card").click((element) => {
let id = element.currentTarget.id;
// 在 set 裡面,表示原本就被選中了,所以要取消選擇
// 否則就選中那張牌
if (selectedCards.has(id)) {
unselectCard(id);
}
else {
selectCard(id);
}
})
}
// 反著查回去
const SYMBOLS = {
"♠️" : "SPADE",
"♥" : "HEART",
"♦" : "DIAMOND",
"♣" : "CLUB",
"1" : "ACE",
"2" : "TWO",
"3" : "THREE",
"4" : "FOUR",
"5" : "FIVE",
"6" : "SIX",
"7" : "SEVEN",
"8" : "EIGHT",
"9" : "NINE",
"10" : "TEN",
"J" : "JACK",
"Q" : "QUEEN",
"K" : "KING",
};
function play() {
let cards = Array.from(selectedCards);
cards = cards.map((val) => {
let tmp = val.split("-");
let card = {
suit : SYMBOLS[tmp[1]],
number : SYMBOLS[tmp[2]],
};
return card;
})
websocket.send("/play", {
action : "play",
playedCards : cards,
});
}
function pass() {
websocket.send("/play", {
action : "pass",
});
}
subscribePlay()
:
function subscribePlay() {
websocket.subscribe("/user/queue/play", (response) => {
response = JSON.parse(response.body);
if (response.message === "success") {
// 成功的話,更新手牌,並且更新其他人的牌數資訊
websocket.send("/my-hands", {});
websocket.send("/hands-info", {});
}
else {
// 如果失敗要顯示錯誤訊息,這邊留給讀者發揮~~
}
});
}
$(document).ready()
,修改後為:
$(document).ready(() => {
websocket.connect('/connect', () => {
subscribeMyHands();
subscribeHandsInfo();
subscribePlay();
websocket.send("/my-hands", {});
websocket.send("/hands-info", {});
});
})
game.html
,我們還需要綁定按鈕,修改後的完整內容為:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout.html}"
>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div layout:fragment="content" class="card game-window">
<input id="my-username" type="hidden" th:value="${username}">
<div class="timer text-center">15</div>
<div class="other-hands-90 other-hands-left" id="user-3"></div>
<div class="other-hands" id="user-4"></div>
<div class="my-hands" id="user-1">
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
</div>
<div class="other-hands-90 other-hands-right" id="user-2"></div>
<div class="card-type">
<div class="m-card text-center">
<div>
♠️
<br>
10
</div>
</div>
<div class="m-card text-center">
<div>
♠️
<br>
J
</div>
</div>
<div class="m-card text-center">
<div>
♠️
<br>
Q
</div>
</div>
<div class="m-card text-center">
<div>
♠️
<br>
K
</div>
</div>
<div class="m-card text-center">
<div>
♠️
<br>
A
</div>
</div>
</div>
<!-- 綁定按鈕的動作 -->
<div class="action">
<button type="button" class="btn btn-outline-success btn-lg" id="button-play" onclick="play()" disabled>出牌</button>
<button type="button" class="btn btn-outline-danger btn-lg" id="button-pass" onclick="pass()" disabled>PASS</button>
</div>
</div>
<div layout:fragment="js-and-css">
<!-- websockets -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"
integrity="sha512-tL4PIUsPy+Rks1go4kQG8M8/ItpRMvKnbBjQm4d2DQnFwgcBYRRN00QdyQnWSCwNMsoY/MfJY8nHp2CzlNdtZA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<!-- custom -->
<script type="text/javascript" th:src="@{/js/WebSockets.js}"></script>
<script type="text/javascript" th:src="@{/js/game.js}"></script>
<link th:href="@{/css/game.css}" rel="stylesheet">
</div>
</body>
</html>
出牌其實很容易,我們原先就定義了 Player 有個方法 play()
用來出牌,再來我們只要串接 WebSocket,取得前端選擇的牌並打出去即可。
但是要讓大家輪流打牌就稍微複雜點,因為還要決定現在輪到誰,尤其是選擇第一個出牌的玩家,會更麻煩,但不至於做不到!
findFirstPlayer()
,加入的程式碼為:
public String findFirstPlayer(String roomId) {
Player[] players = this.gameStatus.getPlayers(roomId);
ArrayList<Card> hands;
for (Player player : players) {
hands = player.getHands();
for (Card card : hands) {
if (card.getSuit().equals(Suit.CLUB) && card.getNumber().equals(Number.THREE)) {
return player.getName();
}
}
}
return null;
}
initializeGameStatus()
,此函數修改後的完整內容為:
public void initializeGameStatus(String roomId) {
Room room = roomList.getRoomById(roomId);
Deck deck = new Deck();
deck.shuffle();
ArrayList<ArrayList<Card>> hands = deck.deal();
ArrayList<String> roomMembers = room.getAllMembers();
Player[] players = new Player[4];
for (int i=0; i<4; i++) {
Player newPlayer = new Player(roomMembers.get(i));
newPlayer.setHands(hands.get(i));
players[i] = newPlayer;
}
gameStatus.add(roomId);
gameStatus.setPlayers(roomId, players);
gameStatus.setCurrentPlayer(roomId, this.findFirstPlayer(roomId));
}
package com.example.game;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class GameTimerList {
Map<String, GameTimer> timerList = new HashMap<>();
public GameTimer get(String roomId) {
return this.timerList.get(roomId);
}
public void add(String roomId, GameTimer timer) {
this.timerList.put(roomId, timer);
}
public void remove(String roomId) {
this.timerList.remove(roomId);
}
}
get()
:取得該房間的 Timeradd()
:新增該房間的 Timerremove()
:移除該房間的 Timerplay()
,內容為:
public void play(String roomId) {
Player[] players = gameStatus.getPlayers(roomId);
int index = 0;
// 找出目前出牌的玩家的 index
for (int i=0; i<4; i++) {
if (players[i].getName().equals(this.gameStatus.getCurrentPlayer(roomId))) {
index = i;
break;
}
}
String currentPlayer;
String previousPlayer = null;
GameTimer timer = new GameTimer();
this.gameTimerList.add(roomId, timer);
// 延遲 5 秒再開始,這是為了等 Client 載入頁面、連接 webscoket 等動作
try {
Thread.sleep(5000);
}
catch(Exception e) {
System.out.println(e.getMessage());
}
// 開始無窮迴圈,直到有人贏為止
while (true) {
currentPlayer = players[index].getName();
this.gameStatus.setCurrentPlayer(roomId, currentPlayer);
// 如果目前的玩家跟上一次出牌的玩家一樣,就說明其他玩家都 pass 了一輪,那這時候就可以自由出牌
if (currentPlayer.equals(previousPlayer)) {
this.gameStatus.setPreviousPlayer(roomId, null);
this.gameStatus.setPreviousPlayedCards(roomId, null);
}
timer.init(20);
// 開始定義計時器每次倒數 1 秒要做什麼事情
timer.countDown((n) -> {
Map<String, Object> status = new HashMap<>();
// 取得當前出牌的玩家
status.put("currentPlayer", this.gameStatus.getCurrentPlayer(roomId));
// 取得上一玩家打出的牌
PlayedCards tmp = this.gameStatus.getPreviousPlayedCards(roomId);
if (tmp == null) {
status.put("previousPlayedCards", null);
}
else {
status.put("previousPlayedCards", tmp.get());
}
// 取得目前 timer 數到幾了
status.put("timer", n);
roomService.sendMessageToRoom(roomId, "/queue/game", status);
});
timer.await(25);
// 如果有玩家的手牌沒了就結束了
// 透過發送結束訊息給所有玩家,使他們結束遊戲,並顯示結算的畫面
if (previousPlayer != null && gameStatus.getHandsByPlayerName(previousPlayer).size() == 0) {
// TODO: 定義結束遊戲的訊息
this.gameStatus.remove(roomId);
this.gameTimerList.remove(roomId);
return;
}
// 新的一輪
previousPlayer = currentPlayer;
gameStatus.setPreviousPlayer(roomId, previousPlayer);
index = (index + 1) % 4;
}
package com.example.game;
import com.example.card.Card;
import com.example.card.PlayedCards;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
@Getter @Setter
public class PlayerActionMessage {
private String action;
private PlayedCards playedCards;
public void setPlayedCards(ArrayList<Card> cards) {
this.playedCards = new PlayedCards(cards);
}
}
setPlayedCards()
是因為我們傳輸過來的格式是一張一張的 Card 組成的 Array,我們可以把他當成一個 ArrayList<Card>
playGame()
加入的片段為:
@MessageMapping("/play")
public void playGame(PlayerActionMessage playerActionMessage, Principal principal) {
String playerName = principal.getName();
String roomId = userStatus.getUserRoomId(playerName);
Player player = this.gameService.findPlayer(roomId, playerName);
Map<String, Object> response = new HashMap<>();
GameTimer timer = gameTimerList.get(roomId);
String action = playerActionMessage.getAction();
if (action.equals("play")) {
PlayedCards currentPlayedCards = playerActionMessage.getPlayedCards();
PlayedCards previousPlayedCards = this.gameStatus.getPreviousPlayedCards(roomId);
// TODO: 檢驗玩家是否能出這組牌
player.play(currentPlayedCards.get());
this.gameStatus.setPreviousPlayer(roomId, playerName);
this.gameStatus.setPreviousPlayedCards(roomId, currentPlayedCards);
}
response.put("message", "success");
simpMessagingTemplate.convertAndSendToUser(playerName, "/queue/play", response);
timer.stop();
}
findPlayer()
到 GameService:
public Player findPlayer(String roomId, String name) {
for (Player player : this.gameStatus.getPlayers(roomId)) {
if (player.getName().equals(name)) {
return player;
}
}
return null;
}
game.js
,加入 subscribeGame()
:
function subscribeGame() {
websocket.subscribe("/user/queue/game", (response) => {
response = JSON.parse(response.body);
$(".timer").text(response.timer);
if (response.currentPlayer === myUsername) {
$("#button-play").prop("disabled", false);
$("#button-pass").prop("disabled", false);
}
else {
$("#button-play").prop("disabled", true);
$("#button-pass").prop("disabled", true);
}
// TODO: 更新上一個玩家打出的牌
});
}
$(document).ready()
:
$(document).ready(() => {
websocket.connect('/connect', () => {
subscribeMyHands();
subscribeHandsInfo();
subscribeGame();
subscribePlay()
websocket.send("/my-hands", {});
websocket.send("/hands-info", {});
});
relocateMyHands();
relocatePlayedCards();
})
其實還有些狀況要處理,比如:檢驗使用者打出的牌是合法的、更新上一個玩家打出的牌等,這些狀況我們明天再來處理~~